Repository navigation
feat(working-memory): measure working memory in admin calibration runs - #9897
Pfannkuchensack wants to merge 23 commits into
Conversation
VAE estimators return a WorkingMemoryEstimate built from fitted and analytic terms; model_on_device accepts it as working_memory=. The AutoencoderKL coefficients and their measured points move to backend/working_memory/data, checked by a data test; reservations are unchanged.
A probe records each estimated model_on_device window's device-level demand inside a calibration scope; results go to a per-device-class store under databases/working_memory. Admin endpoints start, inspect, reset and export runs; the reserved queue origin is refused for ordinary enqueues.
…n fact Environment ids now include windows, linux or other, so calibrations from different operating systems are kept apart; the OS build string moves to soft fact os_build.
# Conflicts: # invokeai/backend/util/vae_working_memory.py # tests/app/invocations/test_flux2_working_memory.py
# Conflicts: # docs/src/content/docs/development/Architecture/model-manager.mdx # invokeai/backend/model_manager/load/load_base.py
# Conflicts: # invokeai/app/services/invocation_services.py
… tests CPU wheels on macOS and Windows have no CUDA allocator binding, so the fixture that scripts a CUDA build scripts its backend too.
JPPhoto
left a comment
There was a problem hiding this comment.
@Pfannkuchensack Your review has arrived. Merge blockers:
-
Pending calibration work can fail during startup.
invocation_services.py:152starts the session processor before the calibration service. A resumed queue item can reachadmit()before the calibration service sets_invokerinworking_memory_calibration_default.py:178; the item then fails before measurement. Start the calibration service before session workers, or remove that ordering dependency. If skipped: a restart with pending calibration work can lose it. Likelihood: only when work is pending at startup. Recovery: rerun the failed request. Test: boot with a pending calibration item and verify it is not failed before service initialization. -
Failed or canceled nodes can produce valid measurements.
session_processor_default.py:314wrapsrun_node()in the probe scope, butrun_node()catches and swallows errors and cancellations atsession_processor_default.py:462. The scope therefore closes without an error, so its windows can remain valid. Propagate the node outcome to the scope. If skipped: failed executions can be treated as trustworthy calibration data. Likelihood: whenever a measured node fails or is canceled. Recovery: remove the affected measurements and rerun. Test: fail and cancel an invocation throughDefaultSessionRunnerand assert its windows are invalid. -
An idempotency conflict can delete a run another request is using. In
working_memory_calibration_default.py:314, the request that first creates a run deletes it on enqueue failure. A concurrent request with the same key but different payload can enqueue successfully first; the creator then deletes the shared run. Cancellation during the threaded enqueue has a similar risk. Protect creation and enqueue as one operation, and only discard a run when no enqueue committed. If skipped: queue items can lose their run and measurement provenance. Likelihood: concurrent or canceled same-key requests. Recovery: remove orphan queue items and rerun. Test: race different payloads under one key and cancel during enqueue. -
A key does not uniquely identify the calibration request. The run ID uses user and idempotency key, while the queue payload hash omits the model route. Reusing the same key and graph for model B after model A can return model A’s run as a success. Also, queue receipts expire after seven days, so a later retry can enqueue again into the persistent run. Persist and compare a request fingerprint that includes the model key and batch data. If skipped: results may have the wrong model provenance or duplicate samples. Likelihood: key reuse across models or after receipt expiry. Recovery: discard the incorrect run and retry with a new key. Test: reuse a key across model paths and after receipt expiry.
-
Partial batch admission is reported as success.
working_memory_calibration_default.py:316rejects only when zero items enqueue. The queue can admit only the available capacity, so a multi-item request can complete with a partial sample set. Require all items to enqueue or mark the run incomplete and clean up admitted items. If skipped: incomplete calibration data can appear complete. Likelihood: queue near capacity. Recovery: discard the partial run and rerun. Test: submit multiple batch values with capacity for only one.
Other findings/issues:
-
Counter failures can still yield valid windows.
counters.py:204swallows peak-reset and synchronization errors; callers continue measuring inprobe.py:402andprobe.py:519. Return failure or raise a probe-local error and mark the window invalid. If skipped: stale peaks or unsettled GPU work can contaminate trusted measurements. Likelihood: transient device/backend errors. Recovery: remove the affected point and rerun. Test: make reset or synchronization fail and assert the window is invalid. -
A lock can miss probe hooks during an arm race.
load_base.py:193snapshotsprobe.armedbefore locking. If it reads false, pauses, then the probe arms and opens a window, the lock proceeds without hooks and can contaminate that window. Coordinate the snapshot and hook registration, or ensure the hooks cover the interleaving. If skipped: concurrent model loading can be included in valid demand. Likelihood: narrow thread interleaving during calibration. Recovery: rerun the measurement. Test: use a barrier to force the interleaving. -
The generated API contract omits a new HTTP 400.
session_queue.py:272returns 400 for a reserved origin, but the route declaration and generatedopenapi.json:12884do not list it. Declare the response and regenerate the API artifact. If skipped: generated clients and docs miss a supported error response. Likelihood: client depends on generated contract. Recovery: regenerate and update clients as needed. Test: verify the route’s OpenAPI response includes 400.
…stworthy The session processor starts after every other service; windows of failed or canceled nodes, failed counter resets and locks racing a window are stored invalid. Keyed requests are identified by model, graph and batch data, are created once per run and admit all of their items or none; the reserved-origin 400 is declared.
|
@JPPhoto Thanks for the second pass. All six points are addressed in b012444. Each one has a test that fails without its fix. High: shutdown overlapping old workers
A detached locked record escaping the probe
Retry lineage before admission
Keyed recovery after the model was removed
Reset not invalidating the idempotency key
Store file: the two new fields ( Contract: no API shape or status changes. The OpenAPI descriptions are regenerated. The stack above (#9899, #9910, #9919, #9927, #9946) has these merged in. |
JPPhoto
left a comment
There was a problem hiding this comment.
Thanks; the six findings from the previous review appear fixed.
Merge blockers
-
Medium - Status can say calibration is available when the store cannot be written. In
invokeai/app/services/working_memory_calibration/working_memory_calibration_default.py:671-693, status checks whether the store file can be read, not whether a run can write it. A read-only store can reportavailable=true, then fail run creation with 503. Also,device_class_ids()can let a directoryPermissionErrorescape as 500 (working_memory_calibration_store.py:140-144). Cover read-only and unreadable store directories. -
Medium - A keyed retry can enqueue a second batch after an ambiguous failure. If queue admission succeeds but recording item IDs fails, the run remains with no IDs. If the key is acknowledged after that 503, its receipt expires, and the queue items are cleared or pruned before observation, a repeat falls through to enqueue at
working_memory_calibration_default.py:423-450. Preserve an admission marker or refuse to re-enqueue when admission is uncertain. Test that sequence.
Other issues
-
Low - A completed item can later appear canceled or partial. The queue records completion before the processor persists the terminal calibration observation (
session_processor_default.py:660-675,327-330). If the process stops in between and the completed row is later pruned, the savedin_progressobservation is treated as canceled (working_memory_calibration_default.py:151-152). Test completion followed by pruning before observation. -
Low - A conflicting key can return 404 instead of the documented 409. With a different request that names a missing model,
_prepare_run()looks up the model before queue admission detects the key conflict (working_memory_calibration_default.py:423-450). The endpoint documents 409 for key reuse with another request (invokeai/app/api/routers/working_memory.py:95-100). -
Low - Run summaries can omit a retry's source item.
derive_run_status()usesretried_from_item_idto determine effective status but does not add that source ID to the returneditem_ids(working_memory_calibration_default.py:144-166). If the source is absent from stored, observed, and live IDs, the summary violates its "every queue item" contract.
…in calibration runs Status reports a store it cannot write as unavailable, and a run is marked before its batch is enqueued so a repeat never enqueues it twice. A clean finish is recorded before the queue completes the item, a key conflict answers 409 before the model lookup, and run summaries list a retry's source.
|
@JPPhoto Thanks. All five points are addressed in 1370b23, and each has a test that fails without its fix. 1. Status available on a store it cannot write. Status now checks that the current device class's store can be written. It opens an OS-deleted temporary file in the store folder, or in the nearest existing folder above it, and checks that the lock file is writable. On Windows it also checks that the JSON file is not read-only. A store that fails this reports 2. A second batch after an ambiguous failure. A run now carries 3. A completed item later reading canceled. On a clean finish (no error, not canceled, and the queue row is neither failed nor canceled), the runner now records the item as completed before the queue completes it. The final observation still writes the queue's status. Tests:
5. 404 instead of 409. For a keyed request with no run, the reset check runs first (404), then the receipt check (409), and only then the model lookup. 7. Retry sources missing from Limits, documented:
Store file: one new field with a default; schema version 1. API: no shape or status changes. The OpenAPI descriptions are regenerated. The stack above has this merged in. |
JPPhoto
left a comment
There was a problem hiding this comment.
Findings:
-
Medium - Cross-process requests can return an empty success or enqueue twice. The per-run lock is process-local. If process A records the marked run and pauses before enqueue, process B can return
201withitem_ids=[]atinvokeai/app/services/working_memory_calibration/working_memory_calibration_default.py:437-464; A may then enqueue or remove the run. If two prepared attempts overlap and one resumes after the first receipt is collected, both can enqueue, and the later item IDs can replace the earlier ones (:369-382,492-503). Use cross-process coordination or an explicit in-progress response. Add a two-process regression for both sequences. -
Medium - Final observation can overwrite a saved completion with canceled. The runner records completion before queue completion, but its
finallyblock callsobserve()afterward (invokeai/app/services/session_processor/session_processor_default.py:320-341). If the completed queue item is cleared or pruned in between,observe()records canceled over the saved completion (invokeai/app/services/working_memory_calibration/working_memory_calibration_default.py:619-633,650-655). Preserve an existing completed observation and test that sequence.
A keyed run's creation holds an OS file lock, striped over 256 files, from preparing the run to recording its items, behind a per-run in-process gate. The final observation keeps a recorded completion of an item the queue no longer has.
…waits for its workers Stop now joins the workers, so the wake-up test opens its gates before stopping and runs the stop case on a thread of its own.
|
@JPPhoto Thanks. Both points are addressed in 9f2345c, with tests that fail without the fixes. 1. Cross-process creation
2. Final observation overwriting a saved completion
Also changed: main's #9855 wake-up test. Merging main brought in #9855's
#9855's guarantee is unchanged. Making the workers share one The stack above has this merged in. |
JPPhoto
left a comment
There was a problem hiding this comment.
One more low-priority change:
- Clearing the queue can leave a canceled run marked completed. The runner records completion before it finishes (
invokeai/app/services/session_processor/session_processor_default.py:320-341). If queue clear happens in that interval, it cancels and deletes the row (invokeai/app/services/session_queue/session_queue_default.py:611-621). The final observation then preservescompletedbecause the row is missing (invokeai/app/services/working_memory_calibration/working_memory_calibration_default.py:647-663). Preserve clear cancellation separately from pruning.
…ue completion as canceled The runner tells the final observation whether the queue committed the item's completion, so a cleared row overrides a completion recorded ahead of the queue while a pruned completed one keeps it.
|
@JPPhoto Thanks, fixed in 9f87a27. The fix
Tests
Remaining limit (documented): if completing the item raises after the queue's commit, a prune or clear in the gap before the final observation records The stack above has this merged in. |
Measured on an RTX PRO 6000 Blackwell: NVML's per-process delta equals reserved growth, the first window adds first-use memory. The hardware test asserts the verified nvml method on Linux CUDA; docs note that containers without the host PID namespace fall back to torch-only.
|
Linux CUDA test run. Machine: Verda 2× NVIDIA RTX PRO 6000 Blackwell Server Edition (96 GB, sm_120), driver 580.178.04, Ubuntu 26.04.1, torch 2.13.0+cu130, cuDNN 9.20, Triton 3.7.1. The PR head was compared with its merge base on Linux NVML reader, run on hardware
|
JPPhoto
left a comment
There was a problem hiding this comment.
Findings/issues:
-
invokeai/app/services/session_processor/session_processor_default.py:323-345: The runner savescompletedbefore queue completion. If Clear Queue deletes the row, then the process stops before the finalobserve(),working_memory_calibration_default.py:151-155uses the savedcompletedstatus. The run appears completed even though Clear Queue canceled it. Effect: admins see a false success. Likelihood: rare clear-and-stop race. Recovery: reset the run; the deleted queue row cannot restore the outcome. Test: clear afterobserve_completed(), stop before final observation, then verify status after restart. -
invokeai/app/services/working_memory_calibration/working_memory_calibration_default.py:653-667: If a failed queue row is pruned before final observation, the missing-row path recordscanceledbecause the runner passes onlyqueue_completed. This loses the failure status and error. Effect: admins cannot diagnose the run from its status. Likelihood: narrow race with prune or clear after failure. Recovery: rerun and check server logs. Test: fail an item, prune it beforeobserve(), and verify the run retainsfailedand its error. -
tests/backend/working_memory/test_probe.py:973-975: The Linux CUDA hardware test requires NVML to be visible and verified. In a supported container without the host PID namespace, NVML cannot match this process andcounters.py:181-197falls back totorch-only; the docs describe that fallback atmodel-manager.mdx:1070. The hardware test then fails despite expected product behavior. Effect: false failure in that environment. Likelihood: any CUDA container without host PID visibility. Test: run the slow probe test in such a container; allow the documented fallback or skip the NVML-specific assertion. -
docs/src/content/docs/development/Architecture/model-manager.mdx:1095: The paragraph says a completed item stays completed after a later clear, then says any item removed by Clear Queue is recorded as canceled. These statements conflict. Effect: maintainers get unclear status semantics. Test: clarify the before-completion and after-completion cases in the paragraph.
…tribute submodel windows on reset The runner no longer records completion ahead of the queue and hands the final observation the item as the queue settled it, so a cleared item never reads completed and a pruned failure keeps its error. Resets and the write fence attribute `<key>:<submodel>` windows to their model, and the NVML hardware test accepts the documented torch-only fallback.
|
@JPPhoto Thanks — all four points are addressed in 49d8f8e, each with a test that fails without its fix. 1. Clear Queue, then a stop, left a false 2. A pruned failure lost
3. NVML in containers without host PID visibility. On Linux CUDA the hardware test now asks NVML itself. If it finds no entry for this process, the test asserts the documented 4. The contradictory doc paragraph is rewritten. It now separates three cases: the queue settled the item before it was removed, Clear Queue removed it first, and the runner raised. It also states the stop-then-prune limit. Also from @joshistoast's review on #9899: resets and the write fence now attribute |
JPPhoto
left a comment
There was a problem hiding this comment.
Other findings/issues:
invokeai/app/services/session_processor/session_processor_default.py:315-321, 666-676, 775-777: Terminal outcomes can be lost before observation. If a refusal or node failure is pruned before_on_after_run_session(), the run recordscanceled; if completion commits but status-event handling raises and the row is then pruned, it can recordfailed. Likelihood: narrow prune or event-error race. Recovery: capture each returned terminal item immediately and recover committed status after transition errors. Test both races.invokeai/backend/util/nvml.py:118-122: In a private PID namespace,os.getpid()can numerically match an unrelated host GPU process. NVML can then attribute that process's memory to InvokeAI and mark the measurement verified. Likelihood: rare but possible. Recovery: verify the host PID mapping and fall back to torch-only when it is unknown. Test a colliding PID.
…ust NVML only in the host PID namespace Each terminal queue transition hands the runner the item it settled, and a transition whose announcement raised reads its committed status back. NVML attributes memory only when the process runs in the initial PID namespace; elsewhere the counters fall back to torch-only.
|
@JPPhoto Thanks, both are addressed in b80d534, each with a test that fails without its fix. 1. Terminal outcomes lost before observation. Every terminal queue transition in the runner now hands it the settled item at once. That covers the refusal, a revoked owner's cancel, a failed save of measurements, the node-error fail in Tests: a refusal pruned before the final record, a node failure pruned before the session ended, and a completion whose announcement raised, then pruned. Remaining limit: if the row is pruned in the moment between a raising announcement and the read-back, the item falls back to the runner's error and reads failed. 2. NVML in a private PID namespace. NVML attributes memory only when the process runs in the initial PID namespace, i.e. I did not use Test: a colliding PID in a namespace of its own reads unknown. |
Retain the committed queue snapshot when event publication fails and pruning wins the read-back race. Assert hardware probe metadata from the measured window.
JPPhoto
left a comment
There was a problem hiding this comment.
Must fix
-
MPS reset can retain an earlier peak (
invokeai/backend/working_memory/counters.py:265-267,285-289)- What this is: A background sampler tracks MPS memory peaks between resets.
- Problem: A sample captured before reset can be folded afterward, making the previous operation's memory look like demand from the new one. A controlled interleaving reproduced this: a sample of 100 folded after reset at 10 left the peak at 100.
- Fix: Serialize sampling and folding with resets, and add a barrier-controlled regression test.
- If we skip it: Valid calibration records can contain inflated measurements.
-
A canceled run can become failed (
invokeai/app/services/working_memory_calibration/working_memory_calibration_default.py:681-686)- What this is: When the queue row is gone,
observe()uses the runner's saved terminal item to recover the outcome. - Problem: If Clear Queue leaves a canceled snapshot and a later callback raises,
observe()ignores the canceled snapshot and records the callback error as failure. - Fix: Accept a saved canceled item as terminal before applying the callback error; add a regression test for cancellation, callback failure, and a missing queue row.
- If we skip it: The same canceled run can show as failed depending on callback and prune timing.
- What this is: When the queue row is gone,
Summary
Working-memory estimates can now be checked against what an operation really uses, on the machine that runs it. This PR only records measurements; no estimate changes.
invokeai/backend/working_memory/probe.py)model_on_device(working_memory=...)window records its demand next to its estimate.fdinfoon Linux, and the driver counter on MPS.invokeai/app/services/working_memory_calibration/)POST /api/v2/models/i/{key}/working_memory/calibrate.enqueue_batchrefuses for everyone.databases/working_memory/.Related Issues / Discussions
Builds on #9896.
QA Instructions
pytest -m slow tests/backend/working_memory/test_probe.pypassed on an RTX 4090 (CUDA 13.0) and an RX 9060 XT (ROCm 10, Windows): the window is valid and covers the reserved growth.cudaMallocAsync):flux_vae_encode: reserved 2200 MiB, measured 1840 MiB.flux_vae_decode: reserved 4400 MiB, measured 2912 MiB.openapi.json/schema.ts;pnpm lintandpnpm testininvokeai/frontend/apipass.Review
Resolved findings
finally, and an item that is gone counts as canceled.calibratewas not safe to retry after a lost response.os.replacefailed transiently while another process held the file.availableand the declared error codes now match what the routes do.Remaining limits
fdinfo, MPS and XPU readers follow vendor documentation, untested on hardware; they are markedverified=false.torch.cuda.empty_cache()calls and background offloads bypass the segment boundary. The torch peaks still bound the demand.Compatibility / Rollout
<db_dir>/working_memory/<device_class>.json.Checklist
What's Newcopy (if doing a release after this PR)Need help on this PR? Tag
@codesmith-botwith what you need.